package krao.queryanswering;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map.Entry;

import krao.data.ABox;
import krao.data.Atom;
import krao.data.ConjQuery;
import krao.data.QueryAnswer;
import krao.data.UnionConjQuery;
import krao.db.DBProvider;
import krao.test.QueryParserTest;

import org.apache.log4j.Logger;

public class QueryAnswerer {
	private final static Logger logger = Logger.getLogger(QueryParserTest.class);

	/** Format of entry: variable : encounteredInAtomVar, e.g. x : A.arg1 */
	private static Hashtable<String, String> varsToUnify;
	/** Store for vars, which have already been processed */
	private static HashSet<String> varsUnified;

	private final static String selectString = "SELECT %s AS %s FROM %s ";
	private final static String joinString = " JOIN %s ON %s = %s ";
	private final static String firstWhereString = " WHERE %s  = '%s'   ";
	private final static String nextWhereString = " AND %s  = '%s'   ";

	/*
	 * private static StringBuilder sb = new StringBuilder(); private static
	 * StringBuilder wheresb = new StringBuilder(); private static StringBuilder
	 * joinsb = new StringBuilder();
	 */

	public static QueryAnswer answerQuery(UnionConjQuery ucq, ABox abox) {
		HashSet<QueryAnswer> answers = answerQueryQA(ucq, abox);
		HashSet<String> result = new HashSet<String>();
		String headVariable = null;
		for (QueryAnswer qa : answers) {
			headVariable = qa.getHeadVariable();
			result.addAll(qa.getAnswer());
		}

		QueryAnswer qa = new QueryAnswer(null);
		qa.setHeadVariable(headVariable);
		qa.setAnswer(result);
		return qa;
	}

	private static HashSet<QueryAnswer> answerQueryQA(UnionConjQuery ucq, ABox abox) {
		if (ucq == null || ucq.isEmpty()) {
			return null;
		}

		HashSet<QueryAnswer> answers = new HashSet<QueryAnswer>();

		for (ConjQuery cq : ucq.getQueries()) {
			QueryAnswer qa = answerOneQuery(cq, abox);
			if (qa != null && !qa.isEmpty()) {
				answers.add(qa);
			}
		}
		return answers;
	}

	private static void clean() {
		varsToUnify = new Hashtable<String, String>();
		varsUnified = new HashSet<String>();
	}

	private static void addUsedVariable(String name) {
		varsUnified.add(name);
	}

	private static boolean isVariableUsed(String name) {
		return varsUnified.contains(name);
	}

	/**
	 * Find an answer to one conjunctive query.
	 * 
	 * @param cq
	 *            query to be answered.
	 * @param abox
	 *            abox containing instances of data which allow for answering @cq
	 * @return
	 */
	public static QueryAnswer answerOneQuery(ConjQuery cq, ABox abox) {
		logger.info("Answering query: " + cq.toStringQueryForm());
		QueryAnswer qa = new QueryAnswer(cq);
		clean();

		StringBuilder sb = new StringBuilder();
		StringBuilder wheresb = new StringBuilder();
		boolean completed = false;

		String queryHeadVar = cq.getHead().getFirstArg();

		for (Atom atom : cq.getBody()) {
			String tmp;
			int variablePosition = atom.getArgPosition(queryHeadVar);
			// if this atom has the variable that's in head
			if (variablePosition > 0) {
				tmp = makeSQLAtom(atom.getName(), variablePosition);
				sb.append(String.format(selectString, tmp, queryHeadVar, atom.getName()));
				// this is the first var to unify
				varsToUnify.put(queryHeadVar, tmp);
				completed = true;
				break;
			}
		}
		// we didn't find the right atom
		if (!completed) {
			return null;
		} else {
			// prepare for next check
			completed = false;
		}
		// logger.info("query string after select: " + sb.toString());

		// unify each var in each atom
		while (!varsToUnify.isEmpty()) {
			// get first element varsToUnify
			Entry<String, String> thisVar = varsToUnify.entrySet().iterator().next();
			String tmp;
			for (Atom atom : cq.getBody()) {
				int variablePosition = atom.getArgPosition(thisVar.getKey());
				// if this atom has the variable thisVar
				if (variablePosition > 0) {
					tmp = makeSQLAtom(atom.getName(), variablePosition);

					// this eliminates invalid joins like JOIN A ON A.arg1 =
					// A.arg1
					if (!tmp.equals(thisVar.getValue())) {
						sb.append(String.format(joinString, atom.getName(), tmp,
								thisVar.getValue()));
					}
					if (!atom.isClassAtom()) {
						int otherPosition = 3 - variablePosition;
						if (otherPosition == 1) {
							if (atom.getFirstArgVariable()) {
								if (!isVariableUsed(atom.getFirstArg())) {
									varsToUnify.put(atom.getFirstArg(),
											makeSQLAtom(atom.getName(), otherPosition));
								}
							} else {
								if (atom.getFirstArg() != null) {
									// object on this position, add to where
									if (wheresb.length() == 0) {
										// empty, use first string
										wheresb.append(String
												.format(firstWhereString,
														makeSQLAtom(atom.getName(),
																otherPosition), atom
																.getFirstArg()));
									} else {
										// not first, use next where string
										wheresb.append(String
												.format(nextWhereString,
														makeSQLAtom(atom.getName(),
																otherPosition), atom
																.getFirstArg()));
									}
								}
							}
						} else {
							if (atom.getSecondArgVariable()) {
								if (!isVariableUsed(atom.getSecondArg())) {
									varsToUnify.put(atom.getSecondArg(),
											makeSQLAtom(atom.getName(), otherPosition));
								}
							} else {
								if (atom.getSecondArg() != null) {
									// object on this position, add to where
									if (wheresb.length() == 0) {
										// empty, use first string
										wheresb.append(String
												.format(firstWhereString,
														makeSQLAtom(atom.getName(),
																otherPosition), atom
																.getSecondArg()));
									} else {
										// not first, use next where string
										wheresb.append(String
												.format(nextWhereString,
														makeSQLAtom(atom.getName(),
																otherPosition), atom
																.getSecondArg()));
									}
								}
							}
						}
					}
				}
			}
			varsToUnify.remove(thisVar.getKey());
			addUsedVariable(thisVar.getKey());
			logger.debug("end of one while iteration, vars to unify: "
					+ getHashtableToString(varsToUnify));
		}

		// logger.info("query string after joins: " + sb.toString());
		// logger.info("query string where after joins: " + wheresb.toString());

		String query = sb.toString() + " " + wheresb.toString();

		ResultSet rs = DBProvider.executeQuery(query);

		qa.setAnswerTo(cq);
		qa.setSqlQuery(query);
		qa.setHeadVariable(queryHeadVar);
		try {
			while (rs.next()) {
				qa.addToAnswer(rs.getObject(1).toString());
			}
		} catch (SQLException e) {
			logger.error(e);
			return null;
		}
		return qa;
	}

	public static String makeSQLAtom(String table, int variablePosition) {
		logger.debug(String.format("creating  %s.arg%s", table, variablePosition));
		return String.format("%s.arg%s", table, variablePosition);
	}

	public static String getHashtableToString(Hashtable<?, ?> ht) {
		if (ht == null) {
			return "";
		}
		StringBuilder sb = new StringBuilder("Hashtable output [" + ht.size() + "]:");
		for (Entry<?, ?> pair : ht.entrySet()) {
			sb.append(String.format("%s->%s\n", pair.getKey(), pair.getValue()));
		}
		return sb.toString();
	}
}
